1   /*
2    * Copyright (c) 1997, 2000, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package java.awt.image;
27  
28  import java.awt.color.ColorSpace;
29  import java.awt.geom.Rectangle2D;
30  import java.awt.Rectangle;
31  import java.awt.geom.Point2D;
32  import java.awt.RenderingHints;
33  import sun.awt.image.ImagingLib;
34  
35  /**
36   * This class performs a pixel-by-pixel rescaling of the data in the
37   * source image by multiplying the sample values for each pixel by a scale
38   * factor and then adding an offset. The scaled sample values are clipped
39   * to the minimum/maximum representable in the destination image.
40   * <p>
41   * The pseudo code for the rescaling operation is as follows:
42   * <pre>
43   *for each pixel from Source object {
44   *    for each band/component of the pixel {
45   *        dstElement = (srcElement*scaleFactor) + offset
46   *    }
47   *}
48   * </pre>
49   * <p>
50   * For Rasters, rescaling operates on bands.  The number of
51   * sets of scaling constants may be one, in which case the same constants
52   * are applied to all bands, or it must equal the number of Source
53   * Raster bands.
54   * <p>
55   * For BufferedImages, rescaling operates on color and alpha components.
56   * The number of sets of scaling constants may be one, in which case the
57   * same constants are applied to all color (but not alpha) components.
58   * Otherwise, the  number of sets of scaling constants may
59   * equal the number of Source color components, in which case no
60   * rescaling of the alpha component (if present) is performed.
61   * If neither of these cases apply, the number of sets of scaling constants
62   * must equal the number of Source color components plus alpha components,
63   * in which case all color and alpha components are rescaled.
64   * <p>
65   * BufferedImage sources with premultiplied alpha data are treated in the same
66   * manner as non-premultiplied images for purposes of rescaling.  That is,
67   * the rescaling is done per band on the raw data of the BufferedImage source
68   * without regard to whether the data is premultiplied.  If a color conversion
69   * is required to the destination ColorModel, the premultiplied state of
70   * both source and destination will be taken into account for this step.
71   * <p>
72   * Images with an IndexColorModel cannot be rescaled.
73   * <p>
74   * If a RenderingHints object is specified in the constructor, the
75   * color rendering hint and the dithering hint may be used when color
76   * conversion is required.
77   * <p>
78   * Note that in-place operation is allowed (i.e. the source and destination can
79   * be the same object).
80   * @see java.awt.RenderingHints#KEY_COLOR_RENDERING
81   * @see java.awt.RenderingHints#KEY_DITHERING
82   */
83  public class RescaleOp implements BufferedImageOp, RasterOp {
84      float[] scaleFactors;
85      float[] offsets;
86      int length = 0;
87      RenderingHints hints;
88  
89      private int srcNbits;
90      private int dstNbits;
91  
92  
93      /**
94       * Constructs a new RescaleOp with the desired scale factors
95       * and offsets.  The length of the scaleFactor and offset arrays
96       * must meet the restrictions stated in the class comments above.
97       * The RenderingHints argument may be null.
98       * @param scaleFactors the specified scale factors
99       * @param offsets the specified offsets
100      * @param hints the specified <code>RenderingHints</code>, or
101      *        <code>null</code>
102      */
103     public RescaleOp (float[] scaleFactors, float[] offsets,
104                       RenderingHints hints) {
105         length = scaleFactors.length;
106         if (length > offsets.length) length = offsets.length;
107 
108         this.scaleFactors = new float[length];
109         this.offsets      = new float[length];
110         for (int i=0; i < length; i++) {
111             this.scaleFactors[i] = scaleFactors[i];
112             this.offsets[i]      = offsets[i];
113         }
114         this.hints = hints;
115     }
116 
117     /**
118      * Constructs a new RescaleOp with the desired scale factor
119      * and offset.  The scaleFactor and offset will be applied to
120      * all bands in a source Raster and to all color (but not alpha)
121      * components in a BufferedImage.
122      * The RenderingHints argument may be null.
123      * @param scaleFactor the specified scale factor
124      * @param offset the specified offset
125      * @param hints the specified <code>RenderingHints</code>, or
126      *        <code>null</code>
127      */
128     public RescaleOp (float scaleFactor, float offset, RenderingHints hints) {
129         length = 1;
130         this.scaleFactors = new float[1];
131         this.offsets      = new float[1];
132         this.scaleFactors[0] = scaleFactor;
133         this.offsets[0]       = offset;
134         this.hints = hints;
135     }
136 
137     /**
138      * Returns the scale factors in the given array. The array is also
139      * returned for convenience.  If scaleFactors is null, a new array
140      * will be allocated.
141      * @param scaleFactors the array to contain the scale factors of
142      *        this <code>RescaleOp</code>
143      * @return the scale factors of this <code>RescaleOp</code>.
144      */
145     final public float[] getScaleFactors (float scaleFactors[]) {
146         if (scaleFactors == null) {
147             return (float[]) this.scaleFactors.clone();
148         }
149         System.arraycopy (this.scaleFactors, 0, scaleFactors, 0,
150                           Math.min(this.scaleFactors.length,
151                                    scaleFactors.length));
152         return scaleFactors;
153     }
154 
155     /**
156      * Returns the offsets in the given array. The array is also returned
157      * for convenience.  If offsets is null, a new array
158      * will be allocated.
159      * @param offsets the array to contain the offsets of
160      *        this <code>RescaleOp</code>
161      * @return the offsets of this <code>RescaleOp</code>.
162      */
163     final public float[] getOffsets(float offsets[]) {
164         if (offsets == null) {
165             return (float[]) this.offsets.clone();
166         }
167 
168         System.arraycopy (this.offsets, 0, offsets, 0,
169                           Math.min(this.offsets.length, offsets.length));
170         return offsets;
171     }
172 
173     /**
174      * Returns the number of scaling factors and offsets used in this
175      * RescaleOp.
176      * @return the number of scaling factors and offsets of this
177      *         <code>RescaleOp</code>.
178      */
179     final public int getNumFactors() {
180         return length;
181     }
182 
183 
184     /**
185      * Creates a ByteLookupTable to implement the rescale.
186      * The table may have either a SHORT or BYTE input.
187      * @param nElems    Number of elements the table is to have.
188      *                  This will generally be 256 for byte and
189      *                  65536 for short.
190      */
191     private ByteLookupTable createByteLut(float scale[],
192                                           float off[],
193                                           int   nBands,
194                                           int   nElems) {
195 
196         byte[][]        lutData = new byte[scale.length][nElems];
197 
198         for (int band=0; band<scale.length; band++) {
199             float  bandScale   = scale[band];
200             float  bandOff     = off[band];
201             byte[] bandLutData = lutData[band];
202             for (int i=0; i<nElems; i++) {
203                 int val = (int)(i*bandScale + bandOff);
204                 if ((val & 0xffffff00) != 0) {
205                     if (val < 0) {
206                         val = 0;
207                     } else {
208                         val = 255;
209                     }
210                 }
211                 bandLutData[i] = (byte)val;
212             }
213 
214         }
215 
216         return new ByteLookupTable(0, lutData);
217     }
218 
219     /**
220      * Creates a ShortLookupTable to implement the rescale.
221      * The table may have either a SHORT or BYTE input.
222      * @param nElems    Number of elements the table is to have.
223      *                  This will generally be 256 for byte and
224      *                  65536 for short.
225      */
226     private ShortLookupTable createShortLut(float scale[],
227                                             float off[],
228                                             int   nBands,
229                                             int   nElems) {
230 
231         short[][]        lutData = new short[scale.length][nElems];
232 
233         for (int band=0; band<scale.length; band++) {
234             float   bandScale   = scale[band];
235             float   bandOff     = off[band];
236             short[] bandLutData = lutData[band];
237             for (int i=0; i<nElems; i++) {
238                 int val = (int)(i*bandScale + bandOff);
239                 if ((val & 0xffff0000) != 0) {
240                     if (val < 0) {
241                         val = 0;
242                     } else {
243                         val = 65535;
244                     }
245                 }
246                 bandLutData[i] = (short)val;
247             }
248         }
249 
250         return new ShortLookupTable(0, lutData);
251     }
252 
253 
254     /**
255      * Determines if the rescale can be performed as a lookup.
256      * The dst must be a byte or short type.
257      * The src must be less than 16 bits.
258      * All source band sizes must be the same and all dst band sizes
259      * must be the same.
260      */
261     private boolean canUseLookup(Raster src, Raster dst) {
262 
263         //
264         // Check that the src datatype is either a BYTE or SHORT
265         //
266         int datatype = src.getDataBuffer().getDataType();
267         if(datatype != DataBuffer.TYPE_BYTE &&
268            datatype != DataBuffer.TYPE_USHORT) {
269             return false;
270         }
271 
272         //
273         // Check dst sample sizes. All must be 8 or 16 bits.
274         //
275         SampleModel dstSM = dst.getSampleModel();
276         dstNbits = dstSM.getSampleSize(0);
277 
278         if (!(dstNbits == 8 || dstNbits == 16)) {
279             return false;
280         }
281         for (int i=1; i<src.getNumBands(); i++) {
282             int bandSize = dstSM.getSampleSize(i);
283             if (bandSize != dstNbits) {
284                 return false;
285             }
286         }
287 
288         //
289         // Check src sample sizes. All must be the same size
290         //
291         SampleModel srcSM = src.getSampleModel();
292         srcNbits = srcSM.getSampleSize(0);
293         if (srcNbits > 16) {
294             return false;
295         }
296         for (int i=1; i<src.getNumBands(); i++) {
297             int bandSize = srcSM.getSampleSize(i);
298             if (bandSize != srcNbits) {
299                 return false;
300             }
301         }
302 
303         return true;
304     }
305 
306     /**
307      * Rescales the source BufferedImage.
308      * If the color model in the source image is not the same as that
309      * in the destination image, the pixels will be converted
310      * in the destination.  If the destination image is null,
311      * a BufferedImage will be created with the source ColorModel.
312      * An IllegalArgumentException may be thrown if the number of
313      * scaling factors/offsets in this object does not meet the
314      * restrictions stated in the class comments above, or if the
315      * source image has an IndexColorModel.
316      * @param src the <code>BufferedImage</code> to be filtered
317      * @param dst the destination for the filtering operation
318      *            or <code>null</code>
319      * @return the filtered <code>BufferedImage</code>.
320      * @throws IllegalArgumentException if the <code>ColorModel</code>
321      *         of <code>src</code> is an <code>IndexColorModel</code>,
322      *         or if the number of scaling factors and offsets in this
323      *         <code>RescaleOp</code> do not meet the requirements
324      *         stated in the class comments.
325      */
326     public final BufferedImage filter (BufferedImage src, BufferedImage dst) {
327         ColorModel srcCM = src.getColorModel();
328         ColorModel dstCM;
329         int numBands = srcCM.getNumColorComponents();
330 
331 
332         if (srcCM instanceof IndexColorModel) {
333             throw new
334                 IllegalArgumentException("Rescaling cannot be "+
335                                          "performed on an indexed image");
336         }
337         if (length != 1 && length != numBands &&
338             length != srcCM.getNumComponents())
339         {
340             throw new IllegalArgumentException("Number of scaling constants "+
341                                                "does not equal the number of"+
342                                                " of color or color/alpha "+
343                                                " components");
344         }
345 
346         boolean needToConvert = false;
347 
348         // Include alpha
349         if (length > numBands && srcCM.hasAlpha()) {
350             length = numBands+1;
351         }
352 
353         int width = src.getWidth();
354         int height = src.getHeight();
355 
356         if (dst == null) {
357             dst = createCompatibleDestImage(src, null);
358             dstCM = srcCM;
359         }
360         else {
361             if (width != dst.getWidth()) {
362                 throw new
363                     IllegalArgumentException("Src width ("+width+
364                                              ") not equal to dst width ("+
365                                              dst.getWidth()+")");
366             }
367             if (height != dst.getHeight()) {
368                 throw new
369                     IllegalArgumentException("Src height ("+height+
370                                              ") not equal to dst height ("+
371                                              dst.getHeight()+")");
372             }
373 
374             dstCM = dst.getColorModel();
375             if(srcCM.getColorSpace().getType() !=
376                dstCM.getColorSpace().getType()) {
377                 needToConvert = true;
378                 dst = createCompatibleDestImage(src, null);
379             }
380 
381         }
382 
383         BufferedImage origDst = dst;
384 
385         //
386         // Try to use a native BI rescale operation first
387         //
388         if (ImagingLib.filter(this, src, dst) == null) {
389             //
390             // Native BI rescale failed - convert to rasters
391             //
392             WritableRaster srcRaster = src.getRaster();
393             WritableRaster dstRaster = dst.getRaster();
394 
395             if (srcCM.hasAlpha()) {
396                 if (numBands-1 == length || length == 1) {
397                     int minx = srcRaster.getMinX();
398                     int miny = srcRaster.getMinY();
399                     int[] bands = new int[numBands-1];
400                     for (int i=0; i < numBands-1; i++) {
401                         bands[i] = i;
402                     }
403                     srcRaster =
404                         srcRaster.createWritableChild(minx, miny,
405                                                       srcRaster.getWidth(),
406                                                       srcRaster.getHeight(),
407                                                       minx, miny,
408                                                       bands);
409                 }
410             }
411             if (dstCM.hasAlpha()) {
412                 int dstNumBands = dstRaster.getNumBands();
413                 if (dstNumBands-1 == length || length == 1) {
414                     int minx = dstRaster.getMinX();
415                     int miny = dstRaster.getMinY();
416                     int[] bands = new int[numBands-1];
417                     for (int i=0; i < numBands-1; i++) {
418                         bands[i] = i;
419                     }
420                     dstRaster =
421                         dstRaster.createWritableChild(minx, miny,
422                                                       dstRaster.getWidth(),
423                                                       dstRaster.getHeight(),
424                                                       minx, miny,
425                                                       bands);
426                 }
427             }
428 
429             //
430             // Call the raster filter method
431             //
432             filter(srcRaster, dstRaster);
433 
434         }
435 
436         if (needToConvert) {
437             // ColorModels are not the same
438             ColorConvertOp ccop = new ColorConvertOp(hints);
439             ccop.filter(dst, origDst);
440         }
441 
442         return origDst;
443     }
444 
445     /**
446      * Rescales the pixel data in the source Raster.
447      * If the destination Raster is null, a new Raster will be created.
448      * The source and destination must have the same number of bands.
449      * Otherwise, an IllegalArgumentException is thrown.
450      * Note that the number of scaling factors/offsets in this object must
451      * meet the restrictions stated in the class comments above.
452      * Otherwise, an IllegalArgumentException is thrown.
453      * @param src the <code>Raster</code> to be filtered
454      * @param dst the destination for the filtering operation
455      *            or <code>null</code>
456      * @return the filtered <code>WritableRaster</code>.
457      * @throws IllegalArgumentException if <code>src</code> and
458      *         <code>dst</code> do not have the same number of bands,
459      *         or if the number of scaling factors and offsets in this
460      *         <code>RescaleOp</code> do not meet the requirements
461      *         stated in the class comments.
462      */
463     public final WritableRaster filter (Raster src, WritableRaster dst)  {
464         int numBands = src.getNumBands();
465         int width  = src.getWidth();
466         int height = src.getHeight();
467         int[] srcPix = null;
468         int step = 0;
469         int tidx = 0;
470 
471         // Create a new destination Raster, if needed
472         if (dst == null) {
473             dst = createCompatibleDestRaster(src);
474         }
475         else if (height != dst.getHeight() || width != dst.getWidth()) {
476             throw new
477                IllegalArgumentException("Width or height of Rasters do not "+
478                                         "match");
479         }
480         else if (numBands != dst.getNumBands()) {
481             // Make sure that the number of bands are equal
482             throw new IllegalArgumentException("Number of bands in src "
483                             + numBands
484                             + " does not equal number of bands in dest "
485                             + dst.getNumBands());
486         }
487         // Make sure that the arrays match
488         // Make sure that the low/high/constant arrays match
489         if (length != 1 && length != src.getNumBands()) {
490             throw new IllegalArgumentException("Number of scaling constants "+
491                                                "does not equal the number of"+
492                                                " of bands in the src raster");
493         }
494 
495 
496         //
497         // Try for a native raster rescale first
498         //
499         if (ImagingLib.filter(this, src, dst) != null) {
500             return dst;
501         }
502 
503         //
504         // Native raster rescale failed.
505         // Try to see if a lookup operation can be used
506         //
507         if (canUseLookup(src, dst)) {
508             int srcNgray = (1 << srcNbits);
509             int dstNgray = (1 << dstNbits);
510 
511             if (dstNgray == 256) {
512                 ByteLookupTable lut = createByteLut(scaleFactors, offsets,
513                                                     numBands, srcNgray);
514                 LookupOp op = new LookupOp(lut, hints);
515                 op.filter(src, dst);
516             } else {
517                 ShortLookupTable lut = createShortLut(scaleFactors, offsets,
518                                                       numBands, srcNgray);
519                 LookupOp op = new LookupOp(lut, hints);
520                 op.filter(src, dst);
521             }
522         } else {
523             //
524             // Fall back to the slow code
525             //
526             if (length > 1) {
527                 step = 1;
528             }
529 
530             int sminX = src.getMinX();
531             int sY = src.getMinY();
532             int dminX = dst.getMinX();
533             int dY = dst.getMinY();
534             int sX;
535             int dX;
536 
537             //
538             //  Determine bits per band to determine maxval for clamps.
539             //  The min is assumed to be zero.
540             //  REMIND: This must change if we ever support signed data types.
541             //
542             int nbits;
543             int dstMax[] = new int[numBands];
544             int dstMask[] = new int[numBands];
545             SampleModel dstSM = dst.getSampleModel();
546             for (int z=0; z<numBands; z++) {
547                 nbits = dstSM.getSampleSize(z);
548                 dstMax[z] = (1 << nbits) - 1;
549                 dstMask[z] = ~(dstMax[z]);
550             }
551 
552             int val;
553             for (int y=0; y < height; y++, sY++, dY++) {
554                 dX = dminX;
555                 sX = sminX;
556                 for (int x = 0; x < width; x++, sX++, dX++) {
557                     // Get data for all bands at this x,y position
558                     srcPix = src.getPixel(sX, sY, srcPix);
559                     tidx = 0;
560                     for (int z=0; z<numBands; z++, tidx += step) {
561                         val = (int)(srcPix[z]*scaleFactors[tidx]
562                                           + offsets[tidx]);
563                         // Clamp
564                         if ((val & dstMask[z]) != 0) {
565                             if (val < 0) {
566                                 val = 0;
567                             } else {
568                                 val = dstMax[z];
569                             }
570                         }
571                         srcPix[z] = val;
572 
573                     }
574 
575                     // Put it back for all bands
576                     dst.setPixel(dX, dY, srcPix);
577                 }
578             }
579         }
580         return dst;
581     }
582 
583     /**
584      * Returns the bounding box of the rescaled destination image.  Since
585      * this is not a geometric operation, the bounding box does not
586      * change.
587      */
588     public final Rectangle2D getBounds2D (BufferedImage src) {
589          return getBounds2D(src.getRaster());
590     }
591 
592     /**
593      * Returns the bounding box of the rescaled destination Raster.  Since
594      * this is not a geometric operation, the bounding box does not
595      * change.
596      * @param src the rescaled destination <code>Raster</code>
597      * @return the bounds of the specified <code>Raster</code>.
598      */
599     public final Rectangle2D getBounds2D (Raster src) {
600         return src.getBounds();
601     }
602 
603     /**
604      * Creates a zeroed destination image with the correct size and number of
605      * bands.
606      * @param src       Source image for the filter operation.
607      * @param destCM    ColorModel of the destination.  If null, the
608      *                  ColorModel of the source will be used.
609      * @return the zeroed-destination image.
610      */
611     public BufferedImage createCompatibleDestImage (BufferedImage src,
612                                                     ColorModel destCM) {
613         BufferedImage image;
614         if (destCM == null) {
615             ColorModel cm = src.getColorModel();
616             image = new BufferedImage(cm,
617                                       src.getRaster().createCompatibleWritableRaster(),
618                                       cm.isAlphaPremultiplied(),
619                                       null);
620         }
621         else {
622             int w = src.getWidth();
623             int h = src.getHeight();
624             image = new BufferedImage (destCM,
625                                    destCM.createCompatibleWritableRaster(w, h),
626                                    destCM.isAlphaPremultiplied(), null);
627         }
628 
629         return image;
630     }
631 
632     /**
633      * Creates a zeroed-destination <code>Raster</code> with the correct
634      * size and number of bands, given this source.
635      * @param src       the source <code>Raster</code>
636      * @return the zeroed-destination <code>Raster</code>.
637      */
638     public WritableRaster createCompatibleDestRaster (Raster src) {
639         return src.createCompatibleWritableRaster(src.getWidth(), src.getHeight());
640     }
641 
642     /**
643      * Returns the location of the destination point given a
644      * point in the source.  If dstPt is non-null, it will
645      * be used to hold the return value.  Since this is not a geometric
646      * operation, the srcPt will equal the dstPt.
647      * @param srcPt a point in the source image
648      * @param dstPt the destination point or <code>null</code>
649      * @return the location of the destination point.
650      */
651     public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt) {
652         if (dstPt == null) {
653             dstPt = new Point2D.Float();
654         }
655         dstPt.setLocation(srcPt.getX(), srcPt.getY());
656         return dstPt;
657     }
658 
659     /**
660      * Returns the rendering hints for this op.
661      * @return the rendering hints of this <code>RescaleOp</code>.
662      */
663     public final RenderingHints getRenderingHints() {
664         return hints;
665     }
666 }